/*  This program is free software; you can redistribute it and/or modify it
    under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation; either version 2.1 of the License, or (at
    your option) any later version.
    For more details, see the GNU Lesser General Public License (www.fsf.org
    or the COPYING file somewhere in the package)
 */

#include "BundleMgr.hh"

/*! @file src/OSGi/BundleMgr.cc
    @brief Class OSGi::BundleMgr (non-inline) methods.
    @author @ref Guillaume_Terrissol
    @date 27th December 2009 - 10th April 2014
    @note This file is distributed under the LGPL license.
    Refer to the file COPYING (or http://www.fsf.org) for more information.
 */

#include <dirent.h>

#include <algorithm>
#include <sstream>

#include "CGuard.hh"
#include "BundleInstallerService.hh"
#include "Exception.hh"
#include "ExtensionPointService.hh"
#include "Utils.hh"

namespace OSGi
{
    /// @cond DEVELOPMENT

//------------------------------------------------------------------------------
//                               Helper Functions
//------------------------------------------------------------------------------

    namespace
    {
        /*! Packs a bundle filename list from a folder.
            @param pBundlePath The folder containing the bundles
            @return A list of bundle filenames
            @note A bundle is a file the name of which has the @e .bndl extension
         */
        std::vector<std::string> getBundleList(const std::string& pBundlePath)
        {
            CGuard<DIR>                 lBundleDir{opendir(pBundlePath.c_str()), closedir};

            if (lBundleDir.get() == nullptr)
            {
                throw IOError{"Can't access bundle folder " + pBundlePath};
            }

            std::vector<std::string>    lBundeList;

            // Reminder : readdir() returns a null pointer when every
            // direct element(including folders) has bee ran down.
            while(dirent* lEntry = readdir(lBundleDir.get()))
            {
                std::string lEntryName = lEntry->d_name;

                if (lEntryName.rfind(kBndlExt) != std::string::npos)
                {
                    lBundeList.push_back(pBundlePath + "/" + lEntryName);
                }
            }

            return lBundeList;
        }


        /*! Lower-than [strict] operator.
            @param pL Left operand
            @param pR Right operand
         */
        bool isVersionLessThan(const std::string& pL, const std::string& pR)
        {
            int     lLMaj{};
            int     lLMin{};
            int     lLRev{};
            int     lRMaj{};
            int     lRMin{};
            int     lRRev{};
            char    lDot{};

            std::istringstream{pL} >> lLMaj >> lDot >> lLMin >> lDot >> lLRev;
            std::istringstream{pR} >> lRMaj >> lDot >> lRMin >> lDot >> lRRev;

            if      (lLMaj < lRMaj)
            {
                return true;
            }
            else if (lRMaj < lLMaj)
            {
                return false;
            }
            else
            {
                if      (lLMin < lRMin)
                {
                    return true;
                }
                else if (lRMin < lLMin)
                {
                    return false;
                }
                else
                {
                    return (lLRev < lRRev);
                }
            }
        }
    }


//------------------------------------------------------------------------------
//                               BundleMgr Private
//------------------------------------------------------------------------------


    /*! @brief BundleMgr private implementation.
        @version 0.5
        @internal
     */
    class BundleMgr::Private
    {
    public:

        Private(Context::Ptr pContext); //!< Constructor.

        Context::Ptr    mContext;       //!< Execution context.
    };


    /*! Constructor.
        @param pContext The manager context
     */
    BundleMgr::Private::Private(Context::Ptr pContext)
        : mContext{pContext}
    {
        mContext->properties()->set("osgi.version", OSGI_VERSION);
    }

    /// @endcond

//------------------------------------------------------------------------------
//                              BundleMgr : Actions
//------------------------------------------------------------------------------

    /*! Loads and installs the bundles.
     */
    void BundleMgr::loadBundles()
    {
        // Lists the bundles.
        auto    lBundles    = getBundleList(properties()->get("application.bundleDir"));
        auto    lInstaller  = pthis->mContext->services()->findByTypeAndName<BundleInstallerService>("osgi.core.installer");

        // From the bundles with the same name, selects the ones with the higher version.
        std::map<std::string, std::string>  lVersionedBundles;
        for(const auto& lB : lBundles)
        {
            std::string lName       = lB.substr(0, lB.rfind('_'));
            std::string lVersion    = lB.substr(lB.rfind('_') + 1);
            lVersion.resize(lVersion.rfind(kBndlExt));

            auto    lVB = lVersionedBundles.find(lName);
            if (lVB == end(lVersionedBundles))
            {
                lVersionedBundles[lName] = lVersion;
            }
            else
            {
                // Greater version.
                if (isVersionLessThan(lVB->second, lVersion))
                {
                    lVB->second = lVersion;
                }
            }
        }

        // Installs the selected bundles.
        for(const auto& lB : lVersionedBundles)
        {
            std::string lName = lB.first + "_" + lB.second + kBndlExt;
            try
            {
                if (auto lNewBundle = lInstaller->installBundle(lName))
                {
                    if (lNewBundle->state() != kInstalled)
                    {
                        throw BundleError{"Bundle " + lName + " installation failed"};
                    }
                    if (context()->isVerbose()) context()->out() << "Bundle " << fromWorkingDirectory(lName) << " installed\n";
                }
                else
                {
                    if (context()->isVerbose()) context()->err() << "Failed to install bundle " << lName.substr(lName.rfind('/')) << '\n';
                }
            }
            catch(std::exception& pE)
            {
                if (context()->isVerbose()) context()->err() << "Bundle " << lName << " failure : " << pE.what() << std::endl;
            }
        }
    }


    /*! Resolves and launches the installed bundles.
     */
    void BundleMgr::startBundles()
    {
        // Resolves the bundles.
        Context::BundleList lLoadedBundles = pthis->mContext->bundles();
        Context::BundleList lUnresolvedBundles;
        // Checks dependencies (name + version)
        for(const auto& lBndl : lLoadedBundles)
        {
            if (lBndl->state() == kInstalled)
            {
                if (!lBndl->act(kResolve, pthis->mContext))
                {
                    lUnresolvedBundles.push_back(lBndl);
                }
                else
                {
                    if (context()->isVerbose()) context()->out() << "Bundle " << lBndl->name() << " resolved\n";
                }
            }
        }
        if (0 < lUnresolvedBundles.size())
        {
            if (context()->isVerbose())
            {
                context()->err() << "Unresolved bundles : {\n";
                for(const auto& lBndl : lUnresolvedBundles)
                {
                    context()->err() << "    " << lBndl->name() << '\n';
                }
                context()->err() << "}\n";
            }
        }

        // Starts the resolved bundles.
        Context::BundleList lResolvedBundles    = context()->resolvedBundles();
        for(const auto& lBndl : lResolvedBundles)
        {
            if (lBndl->state() == kResolved) // Bundles may already be active.
            {
                if (!lBndl->act(kStart, pthis->mContext))
                {
                    if (context()->isVerbose()) context()->err() << "Bundle " << lBndl->name() << " not started because of an unknown error\n";
                }
            }
            else
            {
                if (context()->isVerbose()) context()->err() << "Bundle " << lBndl->name() << " not started because already in state " << lBndl->state() << '\n';
            }
        }
    }


    /*! Stops, and uninstall every bundle.
     */
    void BundleMgr::finalize()
    {
        Context::BundleList lResolvedBundles = pthis->mContext->resolvedBundles();
        Context::BundleList lLoadedBundles{lResolvedBundles.rbegin(), lResolvedBundles.rend()};

        // First, stops every loaded bundle.
        for(const auto& lBndl : lLoadedBundles)
        {
            if (lBndl->state() == kActive)
            {
                if (!lBndl->act(kStop, pthis->mContext))
                {
                    if (context()->isVerbose()) context()->err() << "Failed to stop " << lBndl->state() << " bundle " << lBndl->name() << " while finalizing" << std::endl;
                }
            }
        }
        // Then, may unload them.
        for(const auto& lBndl : lLoadedBundles)
        {
            if ((lBndl->state() == kResolved) || (lBndl->state() == kInstalled))
            {
                if (!lBndl->act(kUninstall, pthis->mContext))
                {
                    if (context()->isVerbose()) context()->err() << "Failed to uninstall " << lBndl->state() << " bundle " << lBndl->name() << " while finalizing" << std::endl;
                }
            }
        }
    }


//------------------------------------------------------------------------------
//                            BundleMgr : Components
//------------------------------------------------------------------------------

    /*! @return The execution context
     */
    Context::Ptr BundleMgr::context()
    {
        return pthis->mContext;
    }


    /*! @return The service manager
     */
    ServiceRegistry::Ptr BundleMgr::services()
    {
        return pthis->mContext->services();
    }


    /*! @return The general properties
     */
    Properties* BundleMgr::properties()
    {
        return pthis->mContext->properties();
    }


//------------------------------------------------------------------------------
//                     BundleMgr : Constructor & Destructor
//------------------------------------------------------------------------------

    /*! @param pArgs Arguments allowing tuning the manager behavior (See @ref OSGi_BundleMgr_Args_Page)
        @note Uses the default bundle factory
     */
    BundleMgr::Ptr BundleMgr::make(const Args& pArgs)
    {
        return make(pArgs, std::make_shared<BundleFactory>());
    }


    /*! @param pArgs    Arguments allowing tuning the manager behavior (see @ref OSGi_BundleMgr_Args_Page)
        @param pFactory Custom bundle factor
     */
    BundleMgr::Ptr BundleMgr::make(const Args& pArgs, BundleFactory::Ptr pFactory)
    {
        auto    lMgr = std::make_shared<BundleMgr>(pArgs, pFactory, Key{});

        OSGI_CONNECT(lMgr->context(), OSGI_SIGNAL(bundleResolved), lMgr->context(), OSGI_SLOT(registerResolvedBundle));

        // Services principaux.
        auto    lExtensionPointService  = std::make_shared<ExtensionPointService>();
        lMgr->context()->services()->checkIn("osgi.core.xp", lExtensionPointService);
        OSGI_CONNECT(lMgr->context(), OSGI_SIGNAL(bundleStarted), lExtensionPointService, OSGI_SLOT(onBundleStarted));
        OSGI_CONNECT(lMgr->context(), OSGI_SIGNAL(bundleStopped), lExtensionPointService, OSGI_SLOT(onBundleStopped));

        auto    lBundleInstallerService = std::make_shared<BundleInstallerService>(lMgr->context());
        lMgr->context()->services()->checkIn("osgi.core.installer", lBundleInstallerService);

        return lMgr;
    }


    /*! Constructor.
        @param pArgs    Arguments allowing tuning the manager behavior (see @ref OSGi_BundleMgr_Args_Page)
        @param pFactory Custom bundle factory
     */
    BundleMgr::BundleMgr(const Args& pArgs, BundleFactory::Ptr pFactory, const Key&)
        : pthis{std::make_shared<Context>(pArgs, this, pFactory, Context::Key{})}
    { }


    /*! Destructor
     */
    BundleMgr::~BundleMgr()
    {
        finalize();
    }


//------------------------------------------------------------------------------
//                              Extra Documentation
//------------------------------------------------------------------------------

    /*! @page OSGi_BundleMgr_Args_Page Possible values for BundleMgr::Args parameters
        @li \"checked\"@n checks expected files (osgi.ini, properties, etc.)
        @li \"cleanCache\"@n cleans the cache on startup
        @li \"config=<b>filename</b>"@n uses custom configuration file (reminder:
        default is <b>osgi.ini</b>).@n If <b>filename</b> is empty, no configuration file
        is used (fields may be set with options instead, see below)
        @li \"bundleDir=<b>path</b>\"@n path to bundles (default is <b>bundles</b>)
        @li \"cacheDir=<b>path</b>\"@n path to binary cache (default is <b>cache</b>)
        @li \"tempDir=<b>path</b>\"@n path to temporary data (default is <b>tmp</b>)
        @li \"permDir=<b>path</b>\"@n path to saved data (default is <b>sav</b>)
        @li \"configDir=<b>path</b>\"@n path to other configuration data (default is <b>cfg</b>)
        @li \"out={<b>cout|string|<i>filename</i></b>}\"@n setups the output log
        (resp. std::cout, std::string, @e $tempDir/filename)
        @li \"err={<b>cout|string|<i>filename</i></b>}\"@n setups the error log
        (resp. std::cout, std::string, @e $tempDir/filename)
     */
}
